#! /usr/bin/env python2.5

# Copyright (c) 2010 Patrick Dubroy <pdubroy@gmail.com>
#
# This program is free software; you can redistribute it and/or modify it 
# under the terms of the GNU General Public License version 2 as published 
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for 
# more details.

import atexit
import errno
import logging
import os
import socket
import threading

USING_PYSIDE = int(os.environ.get("USING_PYSIDE", "0"))

if USING_PYSIDE:
	from PySide.QtCore import *
	pyqtSignal = Signal
else:
	from PyQt4.QtCore import *

__all__ = ["Listener", "Client"]

_socket_files = []

def _cleanup():
	for each in _socket_files:
		try:
			os.remove(each)
		except OSError, e:
			pass
	
atexit.register(_cleanup)


class LineReader(QObject):
	"""Reads data line-by-line from a file or socket.
	
	Emits the signal nextLine(str) for each complete line that is read.
	Instances can be used with select.select(), or be used on a background
	thread using read_all(True).
	"""

	# Emitted as each line of the input that is read.
	nextLine = pyqtSignal(str)

	done = pyqtSignal()
	
	BUFSIZE = 4096

	def __init__(self, file=None, socket=None):
		super(LineReader, self).__init__()
		if file is None and socket is None:
			raise Exception("Either file or socket must be specified.")
		self._file = file
		self._socket = socket
		self._buff = ""
		self._thread = None

	def fileno(self):
		"""Return the integer file descriptor.
		
		Implemented so instances can be passed directly to select.select().
		"""
		return self._file.fileno() if self._file else self._socket.fileno()

	def read(self):
		if self._file is not None:
			try:
				data = os.read(self.fileno(), self.BUFSIZE)
			except OSError, e:
				if e.errno == errno.EINTR:
					return True
				raise
		else:
			data = self._socket.recv(self.BUFSIZE)
		logging.info("Received %d bytes on socket", len(data))
		logging.info("data: " + data)

		eof = len(data) == 0 # Check for end-of-file

		lines = (self._buff + data).splitlines(True)

		if eof:
			self._buff = ""
		else:
			# Buffer everything after the last newline.
			self._buff = "" if data[-1] == "\n" else lines.pop()

		for line in lines:
			self.nextLine.emit(line)

		return not eof
		
	def _read_all(self):
		while self.read(): pass
		self.done.emit()

	def read_all(self, spawn_thread=False):
		if spawn_thread:
			self._thread = threading.Thread(None, self._read_all)
			self._thread.daemon = True
			self._thread.start()
		else:
			self._read_all()

	def wait(self):
		if self._thread:
			self._thread.join()

class _Listener(QObject):

	remoteOpenRequest = pyqtSignal(str)

	def __init__(self, sock, *args):
		super(_Listener, self).__init__()
		self._socket = sock
		self._socket.listen(1)
	
	def start(self):
		self._received_quit = False
		thread = threading.Thread(None, self.run)
		thread.daemon = True
		thread.start()

	def _handle_message(self, message):
		message = str(message)
		logging.debug("Connection accepted")
		logging.debug("Listener received message '%s'" % message)
		parts = message.split(" ", 1)
		if parts[0] == "quit":
			self._received_quit = True
		elif parts[0] == "raise":
			self.remoteOpenRequest.emit("")
		elif parts[0] == "open":
			# Using a signal to pass the message to the UI thread
			self.remoteOpenRequest.emit(os.path.abspath(parts[1]))
		else:
			logging.warning("Listener received unrecognized command '%s'" % cmd)
		
	def run(self):
		filename = None
		try:
			filename = self._socket.getsockname()

			while True:
				conn, addr = self._socket.accept()
				reader = LineReader(socket=conn)
				reader.nextLine.connect(self._handle_message)
				reader.read_all()
				if self._received_quit:
					break
		finally:
			self._socket.close()
			if filename: os.remove(filename) # Clean up the UNIX domain socket file
		logging.debug("Exiting listener thread")
		
	def shutdown(self):
		"""Shut down the listener thread. This should be called by an outside
		thread, usually the one that started the listener thread."""

		logging.debug("Attempting to shut down listener thread")
		addr = self._socket.getsockname()
		client_sock = None
		try:
			client_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
			client_sock.connect(addr)
			client_sock.send("quit\n")
		finally:
			if client_sock: client_sock.close()

def _get_pipe_name(config_dir):
	return os.path.join(config_dir, "sock")

def Listener(config_dir):
	listener = None	
	try:
		sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
		filename = _get_pipe_name(config_dir)
		sock.bind(filename)
		_socket_files.append(filename) # Put this in the list to be cleaned up
		listener = _Listener(sock)
	except socket.error, e:
		# If we get "Address already in use", than another process is running
		if e[0] != errno.EADDRINUSE:
			raise
			
	return listener

def Client(config_dir):
	sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
	sock.connect(_get_pipe_name(config_dir))
	return sock

